VerifactuAPI Technical Overview

Diusframi Group
url: https://www.diusframi.es/contacto/
email: diusframiinnovacion@gmail.com

Table of Contents

1. Purpose & Context

VerifactuAPI provides a secure, JSON‑based gateway for submitting invoice records (“Registros de Facturación”, RFs) to the Agencia Estatal de Administración Tributaria (AEAT), per Real Decreto 1007/2023 and Orden Ministerial HAC/1177/2024. It abstracts AEAT’s SOAP interfaces into a modern RESTful service that supports both voluntary (VERI*FACTU) submissions and a non-voluntary (NO-VERI*FACTU) mode for users preferring not to submit under the voluntary regime;i in this mode, the API securely stores records ensuring integrity, conservation, accessibility, legibility, traceability, and immutability as mandated by law.

2. Actors & Entry Points

Both M2M (machine-to-machine) and A2M (application-to-machine) clients authenticate via OAuth 2.0. Before any RF operations:

  1. Enroll

  2. Register Obligados Tributarios (OTs)

Once done, clients may call the main RF endpoints.

--- title: Enrollment & Registration config: layout: elk elk: mergeEdges: false nodePlacementStrategy: NETWORK_SIMPLEX theme: default --- flowchart LR diusframi("Diusframi Group Producer & Distributor") style diusframi fill:#f58404,stroke:#0f1d8d,stroke-width:2px,color:#0f1d8d subgraph Invoicing_Agent["Invoicing Agent Licensed Producer/Distributor"] direction TB m2m("Machine Client Batch Invoicing System") a2m("Application Client Front-end App") end api[["VerifactuApi RFs Processing Service"]] aeat[["AEAT Web-Services External Tax Authority SOAP"]] %% Enrollment flow diusframi --> |"Issues OAuth2 credentials & shared secret"| Invoicing_Agent Invoicing_Agent --> |Enrolls via licensing portal| diusframi %% Registration & processing m2m -->|"OAuth2 POST /obligadoTributario register OTs"| api a2m -->|"OAuth2 POST /obligadoTributario register OTs"| api api -->|SOAP emittance calls| aeat aeat -->|SOAP responses & callbacks| api

2.1 Endpoints

Endpoint Method Purpose
POST /obligadoTributario POST Register a new OT (taxpayer)
PUT /obligadoTributario/{nif} PUT Update OT configuration (mode, email, representative, etc.)
POST /rfs POST Submit up to 1 000 RFs (Alta or Anulación)
GET /rfs/estado GET Retrieve status of a single RF (by NIF + refExterna)
POST /rfs/estado/consulta POST Paginated/status query of multiple RFs
POST /rfs/consulta POST Bulk fetch stored RFs (non-voluntary mode)
GET /rfs/{idRegistro} GET Fetch a stored RF (non-voluntary mode)
POST /res/consulta POST Bulk fetch EventRegisters (REs)
GET /res/{idRegistro} GET Fetch a single EventRegister (RE)
POST /aeat/requerimiento POST Submit stored RFs under official AEAT requirement
POST /aeat/consulta POST Proxy AEAT’s query API via JSON
GET /integridad/encadenamiento/rfs GET Verify audit-chain integrity for all stored RFs (checks chained-hash continuity)
GET /integridad/encadenamiento/res GET Verify audit-chain integrity for all stored EventRegisters (REs)
POST /integridad/registro/rfs POST Check integrity of an array of RFs (validates its hash and XAdES signature)
POST /integridad/registro/res POST Check integrity of an array of REs (validates its hash and XAdES signature)
POST /exportar/res POST Export one or more stored RFs in bulk (JSON or ZIP package)
POST /exportar/res POST Export one or more stored REs in bulk (JSON or ZIP package)
GET /alertas GET Fetch all active alerts
POST /alertas/consulta POST Filtered, paginated alert query

2.2 General Architecture

--- config: layout: elk elk: mergeEdges: false nodePlacementStrategy: NETWORK_SIMPLEX theme: default --- flowchart LR %% External Actors subgraph Clients direction TB m2m("Machine Client OAuth2") a2m("App Client OAuth2") end subgraph AEAT_WEBSERVICE[AEAT Webservice endpoints] aeatVol[["AEAT SOAP Service Voluntary Submission"]] aeatNoVol[["AEAT SOAP Service Under Requirement Submission"]] end subgraph verifactuAPI["VerifactuApi"] %% AWS Elements subgraph Databases otDB[("OTs (OEFs)")] rfsBuffer[(RFs Buffer)] rfsStorage[(RFs Storage)] resStorage[(REs Storage)] rfsStatuses[(RFs Statuses)] alertsDB[(Alerts)] end %% Lambdas subgraph Lambdas verifactuLambdas:::VerifactuStyle@{ label: "Voluntary mode Lambdas", shape: div-rect } noVerifactuLambdas:::NonVoluntaryStyle@{ label: "Non-Voluntary mode lambdas", shape: div-rect } commonLambdas:::CommonStyle@{ label: "Common lambdas", shape: div-rect } end %% EventBridge eventBridge@{ label: "Submittion Queue", shape: st-rect } subgraph Endpoints otPath@{ label: "/obligadoTributario", shape: lean-r } rfsPath@{ label: "/rfs", shape: lean-r } resPath@{ label: "/res", shape: lean-r } aeatPath@{ label: "/aeat", shape: lean-r } integridadPath@{ label: "/integridad", shape: lean-r } exportarPath@{ label: "/exportar", shape: lean-r } alertasPath@{ label: "/alertas", shape: lean-r } end end %% Relationships Clients <-.-> Endpoints Endpoints <==> Lambdas verifactuLambdas <--> eventBridge <-.-> aeatVol eventBridge <--> rfsBuffer Lambdas --> alertsDB & otDB verifactuLambdas --> rfsStatuses commonLambdas <-.-> AEAT_WEBSERVICE noVerifactuLambdas <-.-> aeatNoVol noVerifactuLambdas <--> rfsStorage & resStorage & rfsStatuses %% Styles classDef VerifactuStyle color:#009900,stroke:#009900 classDef NonVoluntaryStyle color:#cccc00,stroke:#cccc00 classDef CommonStyle color:#0066cc,stroke:#0066cc

3. Core Functionality

3.1 Submit Invoice Records

Pre-requisite: Your OT (Obligado Tributario) or OEF (Obligado a Emitir Factura) must be pre-registered via the dedicated onboarding endpoint (not detailed here).

Endpoint: POST /rfs

Request example:

[  
    {
        "factura":{
            "idEmisor":"19978910Y",
            "numeroSerieFactura":"ABC/DEF/GHI.123_456",
            "fechaExpedicionFactura":"01-01-2025"
        },
        "userReference":"e9dc14b7-e68f-4ae8-847c-3cb8af301c9d",
        "nombreRazonEmisor":"Ferretería no.1",
        "subsanacion":false,
        "rechazoPrevio":"S",
        "tipoFactura":"F1",
        "tipoRectificativa":"S",
        "facturasRectificadas":[
            {        
                "idEmisor":"19978910Y",
                "numeroSerieFactura":"ABC/DEF/GHI.123_456",
                "fechaExpedicionFactura":"01-01-2025"
            }
        ],
        "facturasSustituidas":[
            {
                "idEmisor":"19978910Y",
                "numeroSerieFactura":"ABC/DEF/GHI.123_456",
                "fechaExpedicionFactura":"01-01-2025"
            }
        ],
        "importeRectificacion":{
            "baseRectificada":293140,
            "cuotaRectificada":293140,
            "cuotaRecargoRectificado":293140
        },
        "fechaOperacion":"01-01-2025",
        "descripcionOperacion":"Venta de artículos varios.",
        "facturaSimplificadaArt7273":false,
        "facturaSinIdentifDestinatarioArt61d":false,
        "macrodato":false,
        "emitidaPorTerceroODestinatario":"T",
        "tercero":{
            "nombreRazon":"Carmen Española Española",
            "documento":"19978910Y"
        },
        "destinatarios":[
            {
                "nombreRazon":"Carmen Española Española",
                "documento":"19978910Y"
            }
        ],
        "cupon":false,
        "desglose":[
            {
                "impuesto":"01",
                "claveRegimen":"01",
                "tipoOperacion":"S1",
                "tipoImpositivo":56278,
                "baseImponibleOimporteNoSujeto":293140,
                "baseImponibleACoste":293140,
                "cuotaRepercutida":293140,
                "tipoRecargoEquivalencia":56278,
                "cuotaRecargoEquivalencia":293140
            }
        ],
        "cuotaTotal":293140,
        "importeTotal":293140
    },
    {
        "factura": {
            "idEmisor": "19978910Y",
            "numeroSerieFactura": "ABC/DEF/GHI.123_456",
            "fechaExpedicionFactura": "2025-01-01"
        },
        "userReference": "e9dc14b7-e68f-4ae8-847c-3cb8af301c9d",
        "sinRegistroPrevio": false,
        "rechazoPrevio": true,
        "generadoPor": "T",
        "generador": {
          "nombreRazon": "Carmen Española Española",
          "documento": "19978910Y"
        }
    }
]  

Success Response (HTTP 202 Request Accepted)

Upon receiving your POST, the application immediately performs schema and basic business-rule validations and returns a summary of which RFs were accepted or rejected. The accepted array contains per‑OT groups where each RF’s qrLink URL is intended for inclusion on the printed invoice’s QR code. The refExterna value returned must be used for any subsequent status queries of that RF.

This endpoint also triggers an asynchronous callback (if configured) for richer M2M status updates.

Response Example:

{  
    "accepted": [
        {
            "obligadoTributarioId": "19978910Y",
            "registros": [
                {
                    "refExterna": "d70e42ab-ce3b-4f2a-bd21-0fefedc7dc54",
                    "userReference": "e9dc14b7-e68f-4ae8-847c-3cb8af301c9d",
                    "numSerieFactura": "2NRGGKTU5B",
                    "qrLink": "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=19978910Y&numserie=2NRGGKTU5B&fecha=17-03-2025&importe=131.40"
                }
            ]
        }
    ],
    "rejected": [
        {
            "obligadoTributarioId": "19978910Y",
            "cause": "Validation errors",
            "registros": [
                {
                    "userReference": "e9dc14b7-e68f-4ae8-847c-3cb8af301c9d",
                    "numSerieFactura": "2NRGGKTU5B",
                    "cause": "Invalid date format"
                }
            ]
        }
  ]
}  

Asynchronous Callbacks

If you provided X-Callback-URL, VerifactuAPI will POST the final RF statuses as soon as they leave PROCESSING.

Find more information here Webhook Security & HMAC Signature Guide

Voluntary Mode Flow

--- config: layout: elk elk: mergeEdges: false nodePlacementStrategy: SIMPLE theme: default --- flowchart TB %% External Elements userData@{ label: "Invoice-data", shape: docs } soapResponse@{ label: "SOAP Response", shape: document } rfsStatusesJson:::CommonStyle@{ label: "RFs statuses payload", shape: docs } %% External Actors subgraph Clients direction TB m2m("Machine Client OAuth2") a2m("App Client OAuth2") end %% VerifactuApi subgraph VerifactuApi postRFs:::PostStyle@{ label: "POST /rfs \n OAuth2 ", shape: lean-r } rfsBuffer[(RFs Buffer)] rfsStatuses[(RFs Statuses)] otDB[("OTs (OEFs)")] alertsDB[(Alerts)] postRFsHandler:::CommonStyle@{ label: "Validation & OT grouping \n Acceptance/Rejection Response ", shape: div-rect } voluntayRFsProcessing:::VerifactuStyle@{ label: "RFs generation (Buffered) \n RF's status records creation ", shape: div-rect } submissionLambda:::VerifactuStyle@{ label: "Submission Lambda \n SOAP response parsing", shape: div-rect } callbackLambda:::CommonStyle@{ label: "Callback RFs Statuses lambda \n (if url provided)", shape: div-rect } eventBridge@{ label: "Submittion Queue", shape: st-rect } end %% SOAP_Coms rfsData:::VerifactuStyle@{ label: "RFs envelope", shape: docs } %% AEAT aeatVol[["AEAT SOAP Service Voluntary Submission"]] %% Relationships m2m & a2m -.-> userData -.-> postRFs postRFs <--> postRFsHandler -->|SQS FIFO </br> serialization| voluntayRFsProcessing postRFsHandler <-->|Read/Write| alertsDB postRFsHandler <-->|Read| otDB voluntayRFsProcessing -->|Write| rfsStatuses voluntayRFsProcessing -->|Write| rfsBuffer voluntayRFsProcessing -->|Create| eventBridge eventBridge -->|"Triggers"| submissionLambda submissionLambda <-->|Read/Update| rfsBuffer submissionLambda -->|Update| rfsBuffer & rfsStatuses & eventBridge submissionLambda <-->|Read/Write| alertsDB submissionLambda <-->|Read/Update| otDB submissionLambda -.- rfsData -.-> aeatVol aeatVol -.-> soapResponse -.-> submissionLambda submissionLambda -->|invokes| callbackLambda callbackLambda -.-> rfsStatusesJson -.-> m2m & a2m %% Styles classDef PostStyle color:#00CC00,fill:#99FF99,stroke:#00CC00 classDef VerifactuStyle color:#009900,stroke:#009900 classDef CommonStyle color:#0066cc,stroke:#0066cc

Non-Voluntary Mode Flow

--- config: layout: elk elk: mergeEdges: false nodePlacementStrategy: SIMPLE theme: default --- flowchart TB %% External Elements userData@{ label: "Invoice-data", shape: docs } %% External Actors subgraph Clients direction TB m2m("Machine Client OAuth2") a2m("App Client OAuth2") end %% VerifactuApi subgraph VerifactuApi postRFs:::PostStyle@{ label: "POST /rfs \n OAuth2 ", shape: lean-r } rfsStatuses[(RFs Statuses)] alertsDB[(Alerts)] otDB[("OTs (OEFs)")] rfsStorage[(RFs Storage)] resStorage[(REs Storage)] postRFsHandler:::CommonStyle@{ label: "Validation & OT grouping \n Acceptance/Rejection Response ", shape: div-rect } noVoluntayRFsProcessing:::NonVoluntaryStyle@{ label: "RF's generation \n (enchaning + signing) \n RF's status records creation", shape: div-rect } end %% Relationships m2m & a2m -.-> userData -.-> postRFs postRFs <--> postRFsHandler --> noVoluntayRFsProcessing postRFsHandler <-->|Read/Write| alertsDB postRFsHandler <-->|Read| otDB noVoluntayRFsProcessing -->|Write| rfsStorage & resStorage & rfsStatuses noVoluntayRFsProcessing -->|Read/Update| otDB %% Styles classDef PostStyle color:#00CC00,fill:#99FF99,stroke:#00CC00 classDef NonVoluntaryStyle color:#cccc00,stroke:#cccc00 classDef CommonStyle color:#0066cc,stroke:#0066cc

3.2 RF Status query

Clients can retrieve the current status of any RF submitted to the system. RF statuses are:

Final statuses (COMPLETED, ACCEPTED_BY_AEAT, PARTIALLY_ACCEPTED_BY_AEAT, REJECTED_BY_AEAT, FAILED) are volatile: once a client successfully retrieves a final status, the record is logically deleted (immediately or after a brief delay).

Endpoint: GET /rfs/estado

Quickly fetch all RF statuses under the caller’s SIF context.

Success Response (HTTP 200 Registers Statsuses retrieves successfully)

{
  "registerStatus": {
    "refExterna": "4dc73d25-ae56-4f9f-be3f-d38928cec2cd",
    "numeroSerieFactura": "DMLPQ7EJII",
    "estado": "AEAT_PARTIALLY_ACCEPTED",
    "error": {
      "codigo": 4102,
      "descripcion": "Codigo[4102].El XML no cumple el esquema. Falta informar campo obligatorio.: DetalleDesglose"
    }
  }
}

Endpoint: POST /rfs/estado/consulta

Retrieve RF statuses using flexible filters and pagination.

Request Example

{
  "obligadosTributarios": [
    "19978910Y"
  ],
  "refExternas": [
    "b5cfc7f3-65b4-4ce3-814e-da26eea143ad",
    "4dc73d25-ae56-4f9f-be3f-d38928cec2cd"
  ],
  "estados": [
    "FAILED",
    "SENT"
  ],
  "limit": 1000,
  "nextToken": "eyJhbCI6IjEwMCJ9"
}

Success Response (HTTP 200 Registers Statsuses retrieved successfully)

{
  "rfEstados": [
    {
      "obligadoTributario": "A39200019",
      "rfEstados": [
        {
          "refExterna": "4dc73d25-ae56-4f9f-be3f-d38928cec2cd",
          "numeroSerieFactura": "DMLPQ7EJII",
          "estado": "AEAT_PARTIALLY_ACCEPTED",
          "error": {
            "codigo": 4102,
            "descripcion": "Codigo[4102].El XML no cumple el esquema. Falta informar campo obligatorio.: DetalleDesglose"
          }
        }
      ]
    }
  ],
  "nextToken": "eyJhbCI6IjEwMCJ9"
}

3.3 Alerts & Warnings

To comply with the VERI*FACTU requirements, our application includes an alerts system that proactively notifies users of any issues affecting invoice register (RF) handling whether during submission to the AEAT web service or in non-voluntary (local-storage) mode. Alerts can be blocking and non-blocking, these are called Warnings in this API and are intendend to bring information about issues affecting RFs processing but that not necesarily interfere with the processing of other Rfs

How It Works

  1. Detection
  2. Interruption & Notification
    Whenever an endpoint that participates in the RF-processing lifecycle detects one or more alerts, it halts normal execution and returns an HTTP 433 Alerts Detected response. The payload lists each alert’s ID, type, and descriptive message, thereby forcing the client to address any underlying issues before proceeding. In the case of POST/rfs endpoint, for general Alerts the api will react as decribed above, but for thos alerts affecting to a particular OT only, the api will reject the processing of its RFs, stating as cause of rejection the presense of alerts. in case of warning, thhese will be presented to the client in a different field in the response body at appopiate levelattending if they are general or refered to a particular ot

Endpoint GET /alertas

Querying Active Alerts

Success Response (HTTP 200 Alerts retrieved successfully)

{
  "generales": {
    "alertas": [
      {
        "idAlerta": "ALERT12345",
        "tipoAlerta": "Sistema",
        "mensaje": "Unresolved discrepancies detected in the submission process.",
        "generationTimestamp": "2025-05-23T09:55:31.778Z"
      }
    ],
    "avisos": [
      {
        "idAviso": "ALERT12345",
        "mensaje": "RFs submission are temporary on pause due to a malfunction.",
        "generationTimestamp": "2025-05-23T09:55:31.778Z"
      }
    ]
  },
  "porOT": [
    {
      "obligadoTributario": "19978910Y",
      "alertas": [
        {
          "idAlerta": "ALERT12345",
          "tipoAlerta": "Sistema",
          "mensaje": "Unresolved discrepancies detected in the submission process.",
          "generationTimestamp": "2025-05-23T09:55:31.778Z"
        }
      ],
      "avisos": [
        {
          "idAviso": "ALERT12345",
          "mensaje": "RFs submission are temporary on pause due to a malfunction.",
          "generationTimestamp": "2025-05-23T09:55:31.778Z"
        }
      ]
    }
  ],
  "nextToken": "eyJhbCI6IjEwMCJ9"
}

Endpoint POST alertas/consulta

Query active alerts in your Sistema Informático de Facturación (SIF) context using flexible filters.

Request Example:

{
  "generales": true,
  "obligadoTributario": [
    "A39200019"
  ],
  "tipo": [
    "INTEGRITY",
    "OTHER"
  ],
  "limit": 1000,
  "nextToken": "eyJhbCI6IjEwMCJ9"
}

Success Response (HTTP 200 Alerts retrieved successfully)

{
  "generales": {
    "alertas": [
      {
        "idAlerta": "ALERT12345",
        "tipoAlerta": "Sistema",
        "mensaje": "Unresolved discrepancies detected in the submission process.",
        "generationTimestamp": "2025-05-23T09:55:31.778Z"
      }
    ],
    "avisos": [
      {
        "idAviso": "ALERT12345",
        "mensaje": "RFs submission are temporary on pause due to a malfunction.",
        "generationTimestamp": "2025-05-23T09:55:31.778Z"
      }
    ]
  },
  "porOT": [
    {
      "obligadoTributario": "19978910Y",
      "alertas": [
        {
          "idAlerta": "ALERT12345",
          "tipoAlerta": "Sistema",
          "mensaje": "Unresolved discrepancies detected in the submission process.",
          "generationTimestamp": "2025-05-23T09:55:31.778Z"
        }
      ],
      "avisos": [
        {
          "idAviso": "ALERT12345",
          "mensaje": "RFs submission are temporary on pause due to a malfunction.",
          "generationTimestamp": "2025-05-23T09:55:31.778Z"
        }
      ]
    }
  ],
  "nextToken": "eyJhbCI6IjEwMCJ9"
}

Other endpoints in this API

Endpoint: POST /obligadoTributario

Creates a new ObligadoTributario (taxpayer) record in the system.

Request

Body: JSON object with all required fields to define the new ObligadoTributario (e.g. NIF, name, email, submission mode, optional representative data).

Endpoint: PUT /obligadoTributario/{nif}

Updates an existing ObligadoTributario’s details and configuration (e.g. email, submission mode, representative).

Path Parameter

Request

Body: JSON object with one or more updatable fields (email, remisionVoluntaria, representante).

Endpoint: GET /rfs/estado

Retrieves the current processing status of a single invoice register (RF) by its external reference and issuer NIF.

Query Parameters

Endpoint: POST /rfs/estado/consulta

Bulk-query the processing status of multiple invoice registers (RFs), filtered by taxpayer NIFs, RF identifiers, and/or status values. Supports pagination.

Request

JSON body with any combination of:

Endpoint: POST /aeat/consulta

Forward a query for invoice registers (RFs) to the AEAT web service and return its response (either data or fault).

Request

JSON body conforming to the ConsultaRequest schema, containing filters such as date ranges, taxpayer NIFs, periods, etc., as defined by AEAT’s consultation API.

Endpoint: GET /alertas

Retrieve all currently active alerts across your SIF context, both global alerts and those scoped per taxpayer (OT).

Request

Optional query parameters for pagination:

Endpoint: POST /alertas/consulta

Retrieve active alerts filtered by one or more criteria within your SIF context.

Request

JSON body (at least one filter required):

Endpoint: GET /rfs/{idRegistro}

Fetch a single invoice register (RF) by its unique idRegistro when operating in non-voluntary mode.

Endpoint: POST /rfs/consulta

Retrieve the full details of one or more stored invoice registers (RFs) when operating in non-voluntary mode.

Endpoint: GET /res/{idRegistro}

Fetch a single event register (RE) by its unique idRegistro when operating in non-voluntary mode.

Endpoint: POST /res/consulta

Retrieve one or more event registers (REs) matching supplied filters when operating in non-voluntary mode.

Endpoint /aeat/requerimiento

Non-voluntary submission of stored invoice registers (RFs) to the AEAT web service under official requirement.

Query Parameters: